Repository navigation
Fix RTL audit findings across utility modules - #4
Merged
Merged
Conversation
- f_smallest(t_natural_arr) always returned 0 (accumulator started at 0) - f_div_ceil_2pwr rounded up by unit increments capped at 64 iterations, returning non-power-of-two values for large ratios - f_div_ceil(time, time) truncated sub-ns remainders when computing the mod - f_is_power_of_two failed on 0 and relied on to_signed truncation - f_string_format lost the carry when the fraction rounded up to 10**precision - f_string_substr bounds check was inverted, flagging valid substrings - the unsupported-operation assert in f_vector_tree used 'assert true' and could never fire; it also referenced the pre-rename package name
The third toggle synchronizer stage sampled the first stage instead of the second, and the edge detector XORed against the first stage, which samples the asynchronous toggle directly: a metastable event could double or drop the measurement window. The reported frequency also missed the cycle that closes the window (off by one), and the ready synchronizer chain was not reset, so a reset while the toggle was high produced a spurious output update. The testbench now checks the second, steady-state window, whose count does not depend on the reset phase.
bitsum_o was f_ceil_log2(g_din_w) bits wide, but counting the ones of an all-ones input yields g_din_w, which needs f_ceil_log2(g_din_w + 1) bits: for power-of-two input widths the result wrapped to zero.
- g_pulse_overlength = 1 deasserted the output on every idle cycle because the counter idles at 0 and the compare was not qualified by the enable - g_pulse_length = 1 never produced a pulse (0 < 0 is never true); the output is now active for the whole enable window - stretch mode now adds exactly g_pulse_overlength cycles after the input falling edge (was one cycle short) and is retriggered by a new rising edge instead of cutting the output mid-pulse - the input pulse is defined active high; g_out_level only sets the output polarity, and the zero-overlength passthrough honours it - the fixed-length window ignores edges arriving while it is running - the testbench drives an active-high input, checks mode-dependent widths, and skews scheduled transitions off the clock edges so sampling is unambiguous with the combinational input stage
- both pulse counters restarted on every active input cycle, so pulses wider than one clock were delayed from their trailing edge; they now trigger on the leading edge only - reset drove the outputs to '0', which is the active level when the configured pulse polarity is low; reset now drives the inactive level - delay_var: dv_o was never driven for g_delay_max = 1 and in the pulse architecture; a runtime delay of 1 now works in pulse mode; unsupported g_arch_type values (including the never-implemented mem architecture) are rejected at elaboration; superseded commented-out code removed - the delay_var testbench drove the DUT output dv_o instead of dv_i and never checked dv_o; both fixed
Counting down, the counter started from 0 and underflowed through the full register range before ever reaching the watchdog compare. The down direction is now symmetric to up: reset loads g_wd_timer-1, the counter runs down to 0, pulses timer_o and reloads. g_wd_timer is validated against the counter width at elaboration. The testbench load test carried its stimulus commented out and never drove load_i; it now loads a value and checks that counting resumes from it in both directions.
Add async_reg/shreg_extract attributes to the hand-rolled synchronizer chains (ccd_resync, ccd_sync_pulse, async_reset, clock_measure via its own commit, clock_mux) so synthesis keeps the flops discrete and adjacent instead of mapping them to SRL primitives. The clock_mux anti-glitch keep attribute was boolean-typed, which Vivado silently ignores; it is now a string. ccd_resync's meta-levels guard is a hard failure like the other guards. ccd_sync_pulse validates g_delay_len. ccd_sync_bus parks its request edge detector high across reset so a request held asserted through the reset release is not seen as a new rising edge.
- counter: down direction and load tests enabled, watchdog and load tests get explicit configurations - pulse_stretch: active-low output and unit lengths enabled - delay_pulse: multi-cycle input pulses enabled - delay_var: pulse delay of 1 and g_delay_max = 1 enabled - bitsum: power-of-two input widths exercise the all-ones count - document the removed mem architecture in the user guide and record the behavioral changes in the changelog
async_reg/shreg_extract are Xilinx-only and are silently ignored by other tools, so the synchronizer chains now also carry syn_preserve/syn_srlstyle for Synplify Pro and Lattice LSE, and altera_attribute synchronizer identification (plus shift-register recognition off) for Intel Quartus. The clock_mux keep intent is expressed per tool: keep as a string for Vivado, syn_keep for Synplify/LSE, and KEEP ON via altera_attribute for Quartus, restoring the Quartus behavior lost when the boolean keep was converted to a string. Attributes only prevent structural mangling; the crossing paths still need project-level timing constraints, so each module header now carries a ready-to-adapt set_false_path/set_max_delay example per tool and the user guide gains a CDC attributes and timing constraints section.
There was a problem hiding this comment.
Pull request overview
This PR addresses internal RTL audit findings across multiple lm_util utility blocks by fixing functional edge cases, hardening CDC/synchronizer structures for synthesis, and re-enabling/expanding VUnit regression coverage for previously failing configurations.
Changes:
- Fixes functional corner cases in pulse stretching, variable/fixed pulse delaying, counting (incl. down direction), clock measurement, and several
lm_util_pkghelper functions. - Adds vendor-specific synthesis attributes (
async_reg,shreg_extract,syn_preserve,altera_attribute, etc.) to prevent SRL mapping/optimization of synchronizer flops, and documents required project-level timing constraints. - Expands and corrects testbenches plus
sim/scripts/run.pyto cover previously disabled failing parameter sets.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/lm_util_pulse_stretch.vhd | Fixes fixed-length and stretch-mode corner cases; clarifies input/output polarity behavior. |
| src/lm_util_pkg.vhd | Corrects multiple utility functions (min, ceil divisions, power-of-two check, formatting/substr bounds, assert guard). |
| src/lm_util_delay_var.vhd | Fixes reset polarity behavior, drives dv_o in all modes, rejects unsupported architectures, hardens pulse-mode behavior. |
| src/lm_util_delay_pulse.vhd | Triggers delay from leading edge (multi-cycle pulses), fixes reset polarity behavior. |
| src/lm_util_counter.vhd | Makes down-count symmetric to up-count, adds generic validation, adjusts reset/reload behavior. |
| src/lm_util_clock_mux.vhd | Fixes/extends synthesis attributes (keep, synchronizer attributes) and adds constraint guidance. |
| src/lm_util_clock_measure.vhd | Fixes toggle synchronizer staging/edge detect, corrects off-by-one window close, clears ready chain on reset, adds CDC attributes/docs. |
| src/lm_util_ccd_sync_pulse.vhd | Adds CDC attributes and enforces minimum synchronizer depth for metastability reduction. |
| src/lm_util_ccd_sync_bus.vhd | Prevents false edge detect on reset release when request is held asserted. |
| src/lm_util_ccd_resync.vhd | Adds CDC attributes and improves documentation/constraint guidance; strengthens generic check severity. |
| src/lm_util_bitsum.vhd | Widens bitsum_o to avoid wrap on all-ones for power-of-two input widths. |
| src/lm_util_async_reset.vhd | Adds CDC attributes and constraint guidance for async reset resynchronization. |
| sim/tb/tb_vu_lm_util_pulse_stretch.vhd | Updates stimulus and checks for new polarity definition and corrected stretch/fixed-length semantics. |
| sim/tb/tb_vu_lm_util_delay_var.vhd | Fixes TB driving (dv input vs output) and adds dv_o assertions. |
| sim/tb/tb_vu_lm_util_counter.vhd | Adds down-direction and load-mode coverage; updates expectations for new reload behavior. |
| sim/tb/tb_vu_lm_util_clock_measure.vhd | Adjusts TB to check steady-state measurement window (reset-phase independence). |
| sim/tb/tb_vu_lm_util_bitsum.vhd | Updates TB signal width to match widened bitsum_o. |
| sim/scripts/run.py | Re-enables/extends parameter sweeps for previously failing configs; adds new targeted coverage sets. |
| docs/user_guide.md | Updates module descriptions and documents CDC attributes + required timing constraint patterns. |
| CHANGELOG.md | Records breaking interface/behavior changes and summarizes fixes + test coverage improvements. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
2**g_data_w overflows the 32-bit VHDL integer at elaboration for g_data_w >= 31, failing valid configurations. Check the equivalent f_ceil_log2(g_wd_timer) <= g_data_w instead; the boolean 'and' short-circuit keeps f_ceil_log2 from being called when g_wd_timer < 1.
The pulse delay trigger was still level-based (input active and counter idle), so an input held active beyond the configured delay retriggered a new delay right after the output pulse was emitted, duplicating the event. Both lm_util_delay_pulse and the lm_util_delay_var pulse architecture now detect the leading edge against the previous input sample. In the delay_var pulse architecture dv_o now pulses together with the delayed output pulse, consistent with the SRL architecture, instead of mirroring dv_i one cycle late. The delay_pulse unit-delay register gains the reset-to-inactive-level behavior the other configurations already had. Testbenches check that the output pulse is one cycle wide and that an input wider than the delay does not retrigger; new configurations cover g_pulse_width greater than g_delay.
aca-logimentor
added a commit
that referenced
this pull request
Oct 17, 2026
Plan for the remaining work before merging PR #4: review thread replies, GHDL 6.0.0 pinning in CI, structural CDC checks in vendor synthesis, CDC constraint options, changelog and merge method, and the phase B commit sequence with closure criteria.
aca-logimentor
added a commit
that referenced
this pull request
Oct 17, 2026
Decisions taken, the constraint options not adopted with the reason, and the deviations from the plan. Like the plan itself, this commit is dropped before the merge.
CI installed GHDL from the Ubuntu 24.04 archive (4.1), while the reference simulator is GHDL 6.0.0 with the LLVM backend. Install it with ghdl/setup-ghdl pinned to a commit SHA and fail the job when the reported version or backend differ. Pin vunit_hdl in requirements.txt, which both CI and local environments install from. The user guide states the reference simulator and documents the Windows path length failure mode of long VUnit output directories.
The g_arch_type check was skipped for g_delay_max = 1, so any value, including the removed mem architecture, elaborated silently. The check is now unconditional. With g_delay_max = 1 and C_LM_PULSE the unit-delay branch was a plain register: reset drove '0' (the active level for a low-active pulse), dv_o mirrored dv_i, and an input held active was passed through. It now follows the pulse architecture semantics with a delay of one: leading-edge trigger, one-cycle output pulse with dv_o, inactive level on reset. The delay_var testbench checks the reset level and adds a pulse_long_input test; both run for pulse levels 0 and 1, also with g_delay_max = 1 (four of these configurations fail on the previous RTL). sim/scripts/check_elab_failures.py elaborates entities with invalid generics and expects the intended assertion, covering delay_var, counter (g_wd_timer <= 0), ccd_resync and ccd_sync_pulse, with a valid configuration of each as control. CI runs it before the regression.
Parking the request edge detector at '1' during reset hid a request held across the reset release, but it also dropped a request whose resynchronized rising edge reached the input domain on the first cycle after the release: the bus was never latched and out_rdy_o never pulsed. The detector now loads the resynchronized request during reset, which covers both cases. The module had no testbench. tb_vu_lm_util_ccd_sync_bus checks a basic two-transfer sequence, a request held across reset (no transfer) and a request rising right after the release (one transfer), with 2 and 3 synchronizer stages and an output clock faster and slower than the input clock. req_rises_after_reset fails on the previous RTL.
In get_registers *s_din_meta[0] and get_cells */s_din_meta[0], Tcl runs
[0] as a command ("invalid command name 0"), so the ccd_resync header
and user guide examples failed when pasted into an SDC or LDC file. The
ccd_sync_pulse examples were braced but unescaped, so Quartus treated
[0] as a character class instead of the bus index. Brace the patterns
and escape the brackets. The Vivado examples sit inside -filter {...}
and are unchanged.
The altera_attribute "-name KEEP ON" added for the gated clocks stops Quartus Analysis & Synthesis with "Attribute name KEEP is illegal", so lm_util_clock_mux no longer synthesized in Quartus. KEEP is not a legal assignment name, and the Quartus keep intent is already carried by the syn_keep attribute: Quartus 23.1 turns it into IGNORE_LCELL_BUFFERS and REMOVE_REDUNDANT_LOGIC_CELLS off on s_gated_clks. Drop the illegal assignment and update the attribute table of the user guide. Found by the clock_mux_2 vendor synthesis case on Quartus Prime Lite 23.1std (MAX 10), which now passes.
f1be738 cleared the three ready resampling stages with the synchronous reset. On MAX 10 the reset becomes logic between the stages, so Quartus identifies a one-register synchronizer chain (worst-case MTBF about 3.8e3 years at 100 MHz instead of more than 1e9 years for the other chains). Leave the two synchronizer stages and the edge detector stage without reset, and block the output update while rst_n_i is low instead. The edge detector keeps tracking the synchronized level during reset, so the toggle transition forced by a measure-domain reset is still not taken as a valid ready, which is what f1be738 was after. Found by the clock_measure_16 vendor synthesis case on Quartus Prime Lite 23.1std (MAX 10), which now identifies both stages as a user-specified synchronizer. The clock_measure regression passes.
The CDC cases only matched regex hints such as "Register|Slice" in any log, generated scripts included, so they passed even when a chain was packed into an SRL, and a "review" status still exited with code 0. Cases can now declare synchronizer chains and no_shift_register: - Vivado: a Tcl query on the synthesized netlist checks that every chain register is an FD* cell with ASYNC_REG = TRUE, that the expected number of registers exists, and that no SRL* cell was inferred; - Quartus: the flow adds the Fitter and report_metastability (case clocks in a generated SDC, several clocks asynchronous); every chain register must be listed as a user-specified synchronizer, and the Analysis & Synthesis report must show no SHIFT_TAPS packing. Each check writes one line to reports/lm_checks.txt; a failing or missing check fails the case and the run. Regex hints only read vendor reports and can no longer turn a case into "passed". New cases cover ccd_resync (2 and 4 stages), ccd_sync_bus, ccd_sync_pulse, async_reset (2 and 4 stages), clock_measure and clock_mux. Negative cases must fail each kind of check: a chain without attributes, a chain whose signal has another name (test-only entities under tools/synth/negative), and a delay line packed into shift registers. --ignore-expected-failures evaluates them as regular cases. The reference targets are Vivado 2023.1 (xc7a35tcsg324-1) and Quartus Prime Lite 23.1std on MAX 10 (10M08SAE144C8G), where synchronizer identification and the MTBF report work as on Cyclone V. A Quartus family or device, or a Vivado part, that is not installed now stops with an LM_ERROR naming it. Vivado's intermittent "couldn't read file" failure on Windows is retried once.
42413b0 escaped the brackets inside the braced patterns, assuming that Quartus reads [0] there as a character class. Quartus Prime Lite 23.1 does the opposite: in the Timing Analyzer, get_registers {*s_din_meta[0]} matches the first synchronizer stage, while {*s_din_meta\[0\]} matches nothing and the false path is ignored for an empty collection. Braces alone are what the examples need, since they already stop the Tcl command substitution. Restore the literal indices in the ccd_resync and ccd_sync_pulse headers and in the user guide. The Lattice forms follow the same pattern and are not verified.
The constraint examples depended on instance names (*/inst_resync*/...) or on a hand-written port name, and nothing checked that they still matched the RTL. Following option (a) of the completion plan: - every CDC module header (ccd_resync, ccd_sync_pulse, async_reset, clock_measure, clock_mux, and new for ccd_sync_bus) lists Vivado and Quartus constraints that select registers or asynchronous clear pins only by internal signal names, with braced bus indices; the Lattice forms are marked as not verified; - the user guide shows the ccd_resync example in a marked block, explains the bracket rule, and documents the asynchronous clock groups alternative with the result observed on Vivado 2023.1: the clock groups totally override set_max_delay -datapath_only on the ccd_sync_bus data bus, while set_bus_skew is still reported (8 endpoints, 0.687 ns skew); - run_vendor_synth.py loads every Vivado and Quartus example of the headers and of the guide block on a timing netlist with the case clocks and fails on an empty collection, a Tcl error, an exception that is not listed by report_exceptions, an ignored or overridden exception, or (on Quartus) an invalid one; set_bus_skew must appear in report_bus_skew. neg_chain_renamed now also loads the ccd_resync example on a chain named s_sync_ff and must fail the empty-collection check, and ccd_sync_bus_8_clock_groups (Vivado only) must show the override while the bus skew check still passes. A case can be restricted to some tools. The runner also skips the script lines that Vivado echoes into its log when it extracts the failure message, and retries the intermittent "rt-undefined" Vivado failure like the "couldn't read file" one.
CHANGELOG: the Unreleased section becomes v0.1.0, to be tagged at merge. All behavior changes are listed as breaking: bitsum, pulse_stretch, counter, the delay pulse trigger, delay_var unit delay in pulse mode and the g_arch_type check, the failure severity of the ccd_resync and ccd_sync_pulse depth guards, and the lm_util_pkg results of f_div_ceil_2pwr, f_smallest, f_is_power_of_two and f_string_substr. The fixes add ccd_sync_bus (request rising right after reset), clock_measure (no reset logic between synchronizer stages) and the invalid Tcl of the constraint examples; Changed covers the clock_mux keep through syn_keep, the constraint examples, the synthesis checks and the pinned simulator. User guide: a Verified column in the CDC attribute table (Vivado 2023.1 on xc7a35tcsg324-1, Quartus Prime Lite 23.1std on MAX 10 10M08SAE144C8G; Synplify Pro, Lattice LSE and Synplify Pro for Microchip Libero not verified) with what the verification checks, the reference targets and command for the CDC campaign, and the CDC row of the sensitive cases.
A reset could publish a stale measurement after the release. The measured-domain reset forces the ready toggle to 0; when that transition reached the reference domain after the release (single-cycle reset, slow or stopped measured clock) the previous sample was published again. A measurement pending at the reset and the partial first window, counted from the reset release, were published as well. The synchronizer stages keep no reset logic. Validity is qualified around them instead: - measured domain: ticks are ignored until every register of the tick path has been loaded after the reset release, and the first tick only opens the window, so a published window starts and ends on ticks generated after the reset; - reference domain: ready transitions are ignored until every ready synchronizer stage has been loaded after the release and shows the measured-domain reset level '0'. A '1' there is a ready not reset yet, whose forced transition is not a measurement. New testbench cases check every write of the output register, also when the value does not change: none during reset, none before the second tick after the release, and each one carries the count of a complete window. They cover the reported case, a reset at every phase across two windows, reset lengths of 1 to 5 cycles, measured clocks faster and slower than the reference, and a measured clock stopped before, during and after the reset.
cb93e3e rounded the whole value scaled by 10**precision and converted it to an integer, so any value above integer'high / 10**precision wrapped silently: f_string_format(3000000.0, 3) returned "-2147483.352" instead of "3000000.000". The fraction is rounded alone and a fraction that rounds up to 10**precision carries into the integer part. Inputs that cannot be formatted stop with a failure: an integer part above integer'high (also after the carry) and a precision above 9. The math_real reference body returns the floor and round arguments unchanged from integer'high on, so floor never gets a larger argument. Precision 0 gives no decimal point. tb_vu_lm_util_pkg checks large values up to the integer limit, the carry, negative values and precision 0. The guards are checked at elaboration by check_elab_failures.py through a test-only entity under sim/elab.
A check that passes when something is absent took an empty or partial input as proof of absence: with an empty project.map.rpt the Quartus no_shift_register check found no packing panel and passed on a design that packs a shift register. The same held for the Vivado checks on a closed or empty netlist and on an empty report_exceptions -ignored string, and for a check missing from lm_checks.txt. Before such a conclusion the runner now shows that its input is complete, otherwise the check fails with an LM_ERROR detail that fails the case, a negative case included: - Quartus Analysis & Synthesis report: title, summary and messages panels in the table of contents, every listed panel present, a successful status and the end of processing. A valid report without the register packing panel still passes; - Quartus metastability report: as many complete chains as announced; - Quartus exceptions and ignored-constraints reports: recognized content, summary panel closed; - Vivado: an open design with primitive cells before any netlist check, and recognized report_exceptions strings; - every case: lm_checks.txt reports every check the case defines. New negative cases synthesize a real design, then empty or truncate the Quartus map report, close the Vivado design, or empty the Vivado ignored-exceptions report (test_corrupt), and pass only if the run fails with the expected LM_ERROR (expect_error).
- The six CDC sources state next to the Synplify Pro / Lattice LSE attributes, and in their headers, that these attributes (also read by Synplify Pro for Microchip Libero) are not verified, as the user guide does: a single copied unit carries the information. - async_reset and clock_measure headers, and the user guide: the false path on the CLR/clrn pins removes the asynchronous reset on those pins from timing analysis, release included, so no recovery/removal check is made there. The integrator remains responsible for the synchronized release and for verifying the downstream use of the synchronized reset. - f_is_power_of_two has a single return statement (coding standard R20), with the same results; tb_vu_lm_util_pkg checks powers of two up to 2**30, their neighbours, 0 and negative values.
check_elab_failures.py printed the output of failing cases only, and an intermittent local failure could not be traced back to its case. Each case now writes its name, verdict, expectation, return code, start time, duration, work directory, command and full GHDL output to sim/work/elab_checks/<timestamp>/ (ignored by git; --log-dir overrides it), with a summary.txt per run.
check_elab_failures.py failed intermittently on Windows (about one run in five to ten under load): a case that followed a case of the same entity failed at link time with "collect2.exe: error: ld returned 5 exit status" (also 1) and "compilation error", before any VHDL was elaborated. GHDL links <entity>.exe in the work directory shared by all cases, so consecutive cases of an entity relinked the executable the previous case had just run, which can still be held open for a moment (for instance by an on-access scan). Holding that file open reproduces the same error on demand, and the same command with another output name succeeds. Each case now links its own executable (-o elab_case_<n>). The cases still run one after the other; the change only removes the shared output file.
The flushes are longer than the zero-delay testbench needs (it still passes with C_TGL_FLUSH = 2 and C_RDY_FLUSH = 1). The extra stages are the margin for a metastable first synchronizer stage after the reset, which simulation cannot show; the comment next to the constants says so, so that a later change does not shorten them on the strength of the tests.
The fraction digits were round(frac * 10**precision), and the product is itself rounded to the nearest real. When the exact product is just below a decimal half, the rounded product can be that half, and the result was rounded up a second time: f_string_format(0.0065, 3) gave "0.007". 0.0065 is the binary64 value 0.006499999999999999701627562131989179761148989200592041015625, which rounds to "0.006" at precision 3. Its rounded binary64 product with 1000 is exactly 6.5. Against the final reference vectors, the previous body fails 22/1500 and 393/20000 cases; this fix gives zero mismatches. The fraction v_int..C_VAL is exact (Sterbenz); Dekker's product now gives the rounding error of frac * 10**precision exactly, and only a remainder of exactly 0.5 uses its sign. Exact halves still round away from zero, the carry and the range guards are unchanged.
Contract, in the function header, the user guide and the changelog: the exact binary value of the real is rounded to precision digits, exact halves away from zero (C printf rounds them to even). A decimal literal is first the nearest real, so 9.9995 with precision 3 gives "9.999": the real is 9.99949999999999938893..., and the "10.000" of e9e2ca4 came from rounding the scaled product a second time. sim/scripts/string_format_vectors.py writes reference vectors with a fixed seed: the expected string is Decimal(float) quantized with ROUND_HALF_UP, and each value is stored as sign, 53-bit mantissa and binary exponent so that the testbench rebuilds the same real without parsing a literal. The vectors cover random values over 19 orders of magnitude, both signs, precision 0 to 9, doubles next to decimal halves, exact halves (odd / 2**(precision + 1)), carries that add a digit and the reviewed cases. run.py writes 1500 vectors for the fast and 20000 for the full regression to sim/work; the testbench compares every string and counts the mismatches. Mismatches with these vectors: 0 with the current function; 22/1500 and 393/20000 with the body of 18726a8 (before 3a844b5, which was measured on an earlier set without the five double-rounding cases added to the fixed list); 775/1500 and 8902/20000 with the body of e9e2ca4; 250/1500 and 2708/20000 with the body of 5d53232.
A real neg_shift_register map report whose "Registers Packed Into Inferred Megafunctions" panel kept only its title and border (table of contents, summary and end of processing intact) passed no_shift_register with shift_taps_registers=0: the panel was found and its rows were never validated. A table panel that a check reads must now be complete when present, as Quartus Prime Lite 23.1std writes it: border, expected column header, border, at least one data row with as many cells as the header, closing border. Quartus omits a table panel without rows, so a present panel without rows is incomplete too. The rule covers every panel the checks read: register packing (no_shift_register), Report Exceptions Summary (constraints:listed and :valid), the Statistics table of each metastability chain (chain:*) and the Analysis & Synthesis Summary property table (status). A valid report without the packing panel still passes. New negative cases gut the packing panel of a real report: title and border only, column header only, no closing border; each fails with LM_ERROR. The README and the user guide state what the completeness check guarantees (damaged, truncated or unrecognized reports fail) and what it does not (a report edited to stay well formed passes).
rpt_table() returns no rows for an absent panel, and the metastability validator only looked at the error: with the "Statistics" title row removed from the real ccd_resync_2ff report, the parser still found the method and the registers in the remaining rows and the case passed with 8 structural checks. Each caller now handles an absent panel explicitly. Only the register packing panel may be absent (Quartus omits it, and its table of contents entry, when nothing is packed); a missing chain Statistics panel or Analysis & Synthesis Summary is an LM_ERROR, and the exceptions summary already was one unless the report states that no exceptions were found. The unused rpt_panel() helper is removed. Borders are recognized on the whole line, as Quartus 23.1 writes them (runs of '-' between '+'), and every border and row of a table must have its separators in the same columns; "+INVALID BORDER" is rejected. All 141 table panels of the existing real reports follow that format. New negative cases: the chain Statistics panel without its title row, and the packing panel with an invalid closing border; both fail with LM_ERROR. On copies of all 89 real results with structural checks the verdicts of this runner and of the e05a0e0 runner are identical.
With a value such as -1.0e300 and precision 9, the product of the fraction and 10**precision overflowed and GHDL stopped on a bound check failure inside the function, before the range assert was reached: the input was rejected, but not by the explicit check. The input checks now come first. A magnitude of 2**31 or more is rejected by the range assert before any intermediate computation, and an input out of range is not computed further when a tool continues after the assert; the assert after the carry still rejects a value that rounds up past integer'high. The behavior on valid inputs is unchanged: 0 mismatches on the 1500 fast and 20000 full reference vectors. The function header and the user guide state the numeric assumptions of the exact rounding: IEEE 754 binary64 reals with round to nearest, every intermediate operation rounded on its own (no fused multiply-add, no reassociation, no extended precision). The VHDL standard does not require binary64 in every case; the profile verified is GHDL 6.0.0 with the LLVM backend, Vivado Simulator and Questa/ModelSim are not. New elaboration checks: 1.0e300 and -1.0e300, and carries just above integer'high (2147483647.5 with precision 0, 2147483647.9995 with precision 3); the reference vectors add the values just inside the range.
aca-logimentor
force-pushed
the
bugfix/rtl-audit-fixes
branch
from
October 18, 2026 15:11
3fdb278 to
b42e4fc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes functional bugs found during a code review of the library, hardens the
CDC synchronizers for synthesis, verifies them with vendor synthesis, and
closes the coverage gaps that were hiding the bugs. To be tagged
v0.1.0atmerge (see CHANGELOG).
Breaking changes
lm_util_bitsum:bitsum_oisf_ceil_log2(g_din_w + 1)bits wide.lm_util_pulse_stretch: input pulse active high; stretch adds exactlyg_pulse_overlengthcycles and is retriggerable.lm_util_counter: down direction countsg_wd_timer-1 .. 0.lm_util_delay_pulse,lm_util_delay_var: leading-edge trigger, inactivereset level;
lm_util_delay_varwithg_delay_max = 1andC_LM_PULSEfollows the pulse architecture; any
g_arch_typeother thanC_LM_SRLorC_LM_PULSEis rejected at elaboration for everyg_delay_max.lm_util_ccd_resync(g_meta_levels < 2) andlm_util_ccd_sync_pulse(
g_delay_len < 2) stop elaboration withseverity failure.lm_util_clock_measure: after a reset the first value is published onlywhen a complete window has been measured (second tick after the release);
the partial first window is no longer published.
lm_util_pkg: corrected results off_div_ceil_2pwr,f_smallest,f_is_power_of_two,f_string_substr;f_string_formatrounds the exactbinary value of the real, exact halves away from zero (
9.9995withprecision 3 gives
"9.999", the real being 9.99949999999999938893...),has no decimal point with precision 0, and an integer part above
integer'highor a precision above 9 stops with a failure, checkedbefore any intermediate computation (see CHANGELOG). The exact rounding
assumes IEEE 754 binary64 reals with every operation rounded on its own;
verified with GHDL 6.0.0 (LLVM), not with Vivado Simulator or
Questa/ModelSim.
RTL fixes
first stage and the edge detector compared against it; the reported
frequency missed the window-closing cycle. A reset no longer publishes a
stale value: a pending measurement, the ready transition forced by the
measured-domain reset (also after the release with a slow or stopped
measured clock) and pre-reset values in the synchronizer stages are ignored
until the stages have been reloaded after the release. The synchronizer
stages carry no reset logic (Quartus identified a one-stage chain when they
did).
input widths.
register range; the watchdog range check no longer overflows
integer.their trailing edge; reset drove the active level for low-polarity pulses;
dv_owas undriven in two configurations; unit delay in pulse mode.new request, and a request rising right after the release is no longer
dropped.
altera_attribute "-name KEEP ON"; Quartus keeps the gated clocks throughsyn_keep.f_smallest,f_div_ceil_2pwr,f_div_ceil(time,time),f_is_power_of_two,f_string_format(rounding carry, values up tointeger'highwithout overflow, no second rounding when the scaledfraction lands on a decimal half),
f_string_substr,f_vector_tree.Synthesis hardening and constraints
async_reg/shreg_extract(Vivado),altera_attributesynchronizer identification (Quartus) andsyn_preserve/syn_srlstyle(Synplify Pro / Lattice LSE, not verified;stated next to the attributes and in each header).
select registers and pins by internal signal names only; the user guide
explains them and documents the asynchronous clock groups alternative with
the behavior observed on Vivado 2023.1.
tools/synth/run_vendor_synth.pychecks the synchronizer structure on theVivado netlist and the Quartus reports, loads every published Vivado and
Quartus constraint example, and has negative cases for each check. A check
that passes when something is absent first requires an open, non-empty
netlist or a complete, recognized report, otherwise it fails with an
LM_ERROR. A Quartus table panel that a check reads must be complete whenpresent (column header, data rows, whole-line borders aligned with the
rows, closing border). Only the register packing panel may be absent; a
valid report without it still passes. The check detects damaged or unrecognized reports,
not reports edited to stay well formed (see
tools/synth/README.md).lm_util_async_reset,lm_util_clock_measure: the false path on theCLR/clrn pins removes the reset on those pins from timing analysis,
recovery/removal included; the synchronized release and its downstream use
stay with the integrator.
ghdl/setup-ghdlpinned by SHA; VUnitis pinned to 4.7.1; invalid generics are checked by
sim/scripts/check_elab_failures.py.Verification
Runs on the final branch head unless noted.
--std=08, fast regression: 261/261 (local and CI)--std=08, full regression: 390/390f_string_formatinputs of +-1.0e300 and carries just aboveinteger'high; 10 consecutive local runs after the fix of anintermittent link failure on Windows: 10/10
f_string_formatagainst exact reference vectors (Pythondecimal,fixed seed): 0 mismatches in 1500 (fast) and 20000 (full) vectors; the
body before the double-rounding fix, the body of the first
lm_util_pkgfix of this PR and the body onmain(5d53232) fail393, 8902 and 2708 of the 20000
ghdl/setup-ghdl@75eff367(v1.5.1),VUnit 4.7.1, both jobs green
lm_util_clock_measurereset tests: no publication during a reset orbefore the second tick after it, and every published value is a full
window count; reset at every phase across two windows, 1 to 5 cycle
resets, measured clock faster, slower and stopped across the reset
xc7a35tcsg324-1: ccd_resync (2 and 4 stages),ccd_sync_bus, ccd_sync_pulse, async_reset (2 and 4 stages),
clock_measure, clock_mux: every stage an
FD*cell withASYNC_REG = TRUE, no SRL, header constraint examples matched, listedby
report_exceptionsand not ignored. Rerun after the last change tothe Vivado checks: clock_measure_16 (20 checks) and
ccd_resync_2ff (8 checks) passed
10M08SAE144C8G: same modules,every stage a user-specified synchronizer in
report_metastability, noshift-register inference, constraint examples matched, valid and not
ignored. Rerun after the last change to the Quartus checks: clock_measure_16 (18 checks; three
2-register chains, design MTBF above 1 billion years) and
ccd_resync_2ff (8 checks) passed
change to each tool's checks):
chain without attributes (Vivado packs it into
SRL16E, noASYNC_REG; Quartus identifies it only automatically), renamed chain(empty collections), 32x8 delay line (8
SRL*cells on Vivado,SHIFT_TAPSon Quartus); evaluated as regular cases they fail the run.Vivado
neg_chain_renamedpassed on the runner's automatic retry aftera transient Vivado script read error
Synthesis report, a packing panel gutted to its title, to its header
or without its closing border, a packing panel
with an invalid border and a chain Statistics panel without its title
(Quartus), a closed Vivado
design and an empty Vivado ignored-exceptions report each
fail the run with
LM_ERROR; before the fixes the same cases passedthe checks. On copies of all 89 real results with structural checks,
the final runner gives the same verdicts as the runner before the
last change to the report checks
set_max_delay -datapath_onlytotally overridden,set_bus_skewstill reported(8 endpoints, 0.687 ns skew)
attributes: not verified (no licensed tool available)
Known follow-ups (not in this PR)
lm_util_ccd_sync_bus: no deassert acknowledgment; the output domain hasno reset.
pri_arbiter, rr_arbiter.
lm_util_lfsrseed and width handling.Merge notes
Rebase merge. The planning-only commits were dropped from the branch
before the merge, and the last commit dates the v0.1.0 changelog section.
After the rebase: fast regression 261/261, full regression 390/390,
elaboration checks 23/23, CI green.